﻿/*******************************************************************************
 * AAEngine
 * Copyright (c) 2010 Mike Jarosch
 *
 * Original source PushButton Engine:
 * Copyright (C) 2009 PushButton Labs, LLC
 * For more information see http://www.pushbuttonengine.com
 *
 * This file is licensed under the terms of the MIT license, which is included
 * in the "MIT License.txt" file at the root directory of this SDK.
 ******************************************************************************/

using System;
using AAEngine.Engine.Core;

namespace AAEngine.Animation
{
    public class Animator : EventDispatcher
    {
        private Func<object, object, float, float, object> _ease;
        private float _elapsedTime, _duration;
        private int _repeatCount, _totalRepeatCount;
        private object _current, _start, _target;
        private bool _isAnimating;
        private AnimationType _type, _previousType;

        /// <summary>
        /// The current ease function that will be used to animate from start to target value 
        /// </summary>
        public Func<object, object, float, float, object> Ease
        {
            get { return _ease; }
            set { _ease = value; }
        }

        /// <summary>
        /// The amount of time that has passed since the animation started.
        /// </summary>
        public float Elapsed
        {
            get { return _elapsedTime; }
        }

        /// <summary>
        /// The current number times the animation will be repeated.
        /// </summary>
        public int RepeatCount
        {
            get
            {
                if (IsAnimating)
                {
                    return _repeatCount;
                }
                return _totalRepeatCount;
            }
            set
            {
                if (IsAnimating)
                {
                    _repeatCount = value;
                }
                else
                {
                    _totalRepeatCount = value;
                }
            }
        }

        /// <summary>
        /// The current value of the animation.
        /// </summary>
        public object CurrentValue
        {
            get { return _current; }
        }

        /// <summary>
        /// The time it should take to animate from the start value to the target value.
        /// </summary>
        public float Duration
        {
            get { return _duration; }
            set { _duration = value; }
        }

        /// <summary>
        /// Whether or not the animation is currently playing.
        /// </summary>
        public bool IsAnimating
        {
            get { return _isAnimating; }
        }

        /// <summary>
        /// The type of playback to use for the animation.
        /// </summary>
        public AnimationType AnimationType
        {
            get
            {
                if (IsAnimating)
                {
                    return _type;
                }
                return _previousType;
            }
            set
            {
                if (IsAnimating)
                {
                    _type = value;
                }
                _previousType = value;
            }
        }

        /// <summary>
        /// The value the animation should start at.
        /// </summary>
        public object StartValue
        {
            get { return _start; }
            set { _start = value; }
        }

        /// <summary>
        /// The value to animate to.
        /// </summary>
        public object TargetValue
        {
            get { return _target; }
            set { _target = value; }
        }

        /// <summary>
        /// The total number of times to repeat the animation.
        /// </summary>
        public int TotalRepeatCount
        {
            get { return _totalRepeatCount; }
        }

        /// <summary>
        /// Starts playback.
        /// </summary>
        public void Play()
        {
            if (IsAnimating)
            {
                return;
            }

            _isAnimating = true;
            _type = _previousType;
            _previousType = AnimationType.NO_ANIMATION;

            if (_elapsedTime == 0.0f)
            {
                DispatchEvent(new AnimationEvent(AnimationEvent.ANIMATION_STARTED_EVENT, this));
            }
            else
            {
                DispatchEvent(new AnimationEvent(AnimationEvent.ANIMATION_RESUMED_EVENT, this));
            }
        }

        /// <summary>
        /// Resets the animation to all the values it had before it started playing.
        /// </summary>
        public void Reset()
        {
            if (IsAnimating)
            {
                Stop();
            }

            _current = _start;
            _elapsedTime = 0.0f;
            _repeatCount = _totalRepeatCount;
        }

        /// <summary>
        /// Reverses the animation, effectively setting the start value to the target value and
		/// the target value to the start value.
        /// </summary>
        public void Reverse()
        {
            object swap = _target;
            _target = _start;
            _start = swap;

            if (IsAnimating)
            {
                _elapsedTime = Math.Abs(_duration - _elapsedTime);
            }
        }

        /// <summary>
        /// Starts the animation. This is simply a shorthand way to specify all the necessary parameters
		/// for playback. Play can be called instead if the properties are already set.
        /// </summary>
        /// <param name="startValue"></param>
        /// <param name="targetValue"></param>
        /// <param name="duration"></param>
        /// <param name="type"></param>
        /// <param name="repeatCount"></param>
#if WINDOWS_PHONE || XBOX
        public void Start(object startValue, object targetValue, float duration, AnimationType type)
        {
            Start(startValue, targetValue, duration, type, 0);
        }

        public void Start(object startValue, object targetValue, float duration, AnimationType type, int repeatCount)
#else
        public void Start(object startValue, object targetValue, float duration, AnimationType type, int repeatCount = 0)
#endif
        {
            if (IsAnimating)
            {
                Stop();
            }

            _start = startValue;
            _target = targetValue;
            _duration = duration;
            _previousType = type;
            _totalRepeatCount = repeatCount;

            Reset();
            Play();
        }

        /// <summary>
        /// This should be called every frame to perform the animation.
        /// </summary>
        /// <param name="elapsed">The amount of time that has elapsed since the last call to this.</param>
        public void Animate(float elapsed)
        {
            if (_type == AnimationType.NO_ANIMATION)
            {
                return;
            }

            _elapsedTime += elapsed;
            if (_elapsedTime > _duration)
            {
                if (_type == AnimationType.PLAY_ANIMATION_ONCE || _repeatCount == 0)
                {
                    Finish();
                    return;
                }

                if (_type == AnimationType.PING_PONG_ANIMATION)
                {
                    Reverse();
                }
                else
                {
                    _elapsedTime = Math.Abs(_duration - _elapsedTime);
                }

                if (_repeatCount > 0)
                {
                    _repeatCount--;
                }

                DispatchEvent(new AnimationEvent(AnimationEvent.ANIMATION_REPEATED_EVENT, this));
            }

            if (_ease != null)
            {
                _current = DoEase(_start, _target, _elapsedTime, _duration);
            }
            else
            {
                _current = Interpolate(_start, _target, _elapsedTime / _duration);
            }
        }

        /// <summary>
        /// Set the current value to the target value instantly.
        /// </summary>
        public void Finish()
        {
            if (!IsAnimating)
            {
                return;
            }

            if (_type == AnimationType.PING_PONG_ANIMATION && (_repeatCount & 1) == 1)
            {
                _current = _start;
            }
            else
            {
                _current = _target;
            }

            _previousType = _type;
            _type = AnimationType.NO_ANIMATION;
            _isAnimating = false;

            DispatchEvent(new AnimationEvent(AnimationEvent.ANIMATION_FINISHED_EVENT, this));
        }

        /// <summary>
        /// Stops playback. It can be resumed by calling Play.
        /// </summary>
        public void Stop()
        {
            if (!IsAnimating)
            {
                return;
            }

            _previousType = _type;
            _type = AnimationType.NO_ANIMATION;
            _isAnimating = false;

            DispatchEvent(new AnimationEvent(AnimationEvent.ANIMATION_STOPPED_EVENT, this));
        }

        /// <summary>
        /// Applies an ease function
		/// Can be overridden to support easing objects  
        /// </summary>
        /// <param name="start"></param>
        /// <param name="end"></param>
        /// <param name="elasped"></param>
        /// <param name="duration"></param>
        /// <returns></returns>
        protected virtual object DoEase(object start, object end, float elasped, float duration)
        {
            double startD, endD;

            //if (start is string && !Double.TryParse((string)start, out startD))
            //{
            //    Logger.Warn(this, "DoEase", "Unable to convert start value to a double for easing.");
            //    return start;
            //}
            //else if (end is string && !Double.TryParse((string)end, out endD))
            //{
            //    Logger.Warn(this, "DoEase", "Unable to convert end value to a double for easing.");
            //    return end;
            //}
            //else
            //{
            startD = Convert.ToDouble(start);
            endD = Convert.ToDouble(end);
            //}

            return _ease(startD, (endD - startD), elasped, duration);
        }

        /// <summary>
        /// Performs the actual animation. This can be overridden by subclasses to interpolate
        /// more complex types. This default implementation will try and convert to a double and
        /// animate that.
        /// </summary>
        /// <param name="start">The value to interpolate from.</param>
        /// <param name="end">The value to interpolate to.</param>
        /// <param name="time">The interpolation factor. A value of 0 will return the start value, a
        /// value of 1 will return the end value. A value of 0.5 should return the value half
        /// way between start and end.</param>
        /// <returns>The interpolated value.</returns>
        protected virtual object Interpolate(object start, object end, float time)
        {
            double startD, endD;

            //if (start is string && !Double.TryParse((string)start, out startD))
            //{
            //    Logger.Warn(this, "Interpolate", "Unable to convert start value to a double for easing.");
            //    return start;
            //}
            //else if (end is string && !Double.TryParse((string)end, out endD))
            //{
            //    Logger.Warn(this, "Interpolate", "Unable to convert end value to a double for easing.");
            //    return end;
            //}
            //else
            //{
            startD = Convert.ToDouble(start);
            endD = Convert.ToDouble(end);
            //} 
            
            if (time > 1.0f)
            {
                time = 1.0f;
            }

            if ((endD - startD) < 0f)
            {
                return startD - ((startD - endD) * time);
            }

            return startD + ((endD - startD) * time);
        }
    }
}
